#pragma once

#include "../BaseTracker.h"

#include "instrument.h"
#include "position.h"
#include "funds.h"

#include "record.h"
#include "mktpool.h"

namespace mdtx::punch::tradesim
{
    /**
     * @brief Tracks tradesim events
     *
     * @tparam QT quote type
     */
    template <typename QT>
    class Tracker : public BaseTracker<QT>
    {
    public:
        using base = BaseTracker<QT>;
        using typename base::order_type;
        using time_type = typename QT::time_type;
        using id_type = typename QT::id_type;
        using typename base::quant_type;

        using instrument_type = Instrument<QT>;
        using position_type = Position<QT>;
        using funds_type = Funds<QT>;

        using record_type = BaseRecord<QT>;
        using mktpool_type = MktPool<QT>;
        using record_ptr = std::shared_ptr<record_type>;
        using mktpool_ptr = std::shared_ptr<mktpool_type>;

    private:
        size_t n_instr, n_record, n_trade;
        // owned
        instrument_type instrument;
        position_type position;
        funds_type funds;
        // shared
        record_ptr record;
        mktpool_ptr mkt;

        void on_submit_buy(time_type time, id_type cli_id, id_type ins_id, id_type seq_id,
                           quant_type price, quant_type quant, bool limit)
        {
            // record
            n_record += 1;
            auto rec = record_type::create(time, cli_id, ins_id, seq_id,
                                           price, quant, quant_type{},
                                           RecordEvent::submit, true, true);
            record->on_record(cli_id, ins_id, rec);

            // freeze cash
            auto &cash = funds.get(cli_id);
            quant_type tnvr = limit ? price * quant : mkt->get(ins_id).price * quant;
            if (tnvr > cash.avail)
            {
                cash.borrow += tnvr - std::max(cash.avail, quant_type{});
                cash.avail = 0;
            }
            else
            {
                cash.avail -= tnvr;
            }
            record->on_funds(cli_id, ins_id, cash);
        }

        void on_submit_sell(time_type time, id_type cli_id, id_type ins_id, id_type seq_id,
                            quant_type price, quant_type quant, bool limit)
        {
            // record
            n_record += 1;
            auto rec = record_type::create(time, cli_id, ins_id, seq_id,
                                           price, quant, quant_type{},
                                           RecordEvent::submit, false, true);
            record->on_record(cli_id, ins_id, rec);

            // freeze share
            auto &pos = position.get(cli_id, ins_id);
            if (quant > pos.avail)
            {
                pos.borrow += quant - std::max(pos.avail, quant_type{});
                pos.avail = 0;
            }
            else
            {
                pos.avail -= quant;
            }
            record->on_position(cli_id, ins_id, pos);
        }

        void on_match_buy(time_type time, id_type cli_id, id_type ins_id, id_type seq_id,
                          quant_type price, quant_type quant, quant_type tnvr, bool is_aggr)
        {
            // record
            n_trade += 1;
            n_record += 1;
            auto rec = record_type::create(time, cli_id, ins_id, seq_id,
                                           price, quant, tnvr,
                                           RecordEvent::match, true, is_aggr);
            record->on_record(cli_id, ins_id, rec);

            // position
            auto &pos = position.get(cli_id, ins_id);
            pos.tnvr_buy += tnvr;
            pos.quant_buy += quant;
            if (instrument.get(ins_id).T0)
            {
                pos.avail += quant;
            }
            else
            {
                pos.tnew += quant;
            }
            pos.net += quant;
            record->on_position(cli_id, ins_id, pos);

            // cash
            auto &cash = funds.get(cli_id);
            cash.net -= tnvr;
            record->on_funds(cli_id, ins_id, cash);
        }

        void on_match_sell(time_type time, id_type cli_id, id_type ins_id, id_type seq_id,
                           quant_type price, quant_type quant, quant_type tnvr, bool is_aggr)
        {
            // record
            n_trade += 1;
            n_record += 1;
            auto rec = record_type::create(time, cli_id, ins_id, seq_id,
                                           price, quant, tnvr,
                                           RecordEvent::match, false, is_aggr);
            record->on_record(cli_id, ins_id, rec);

            // position
            auto &pos = position.get(cli_id, ins_id);
            pos.tnvr_sell += tnvr;
            pos.quant_sell += quant;
            pos.net -= quant;
            record->on_position(cli_id, ins_id, pos);

            // cash
            auto &cash = funds.get(cli_id);
            cash.avail += tnvr;
            cash.net += tnvr;
            record->on_funds(cli_id, ins_id, cash);
        }

    public:
        Tracker(const instrument_type &ins, const position_type &pos, const funds_type &funds,
                record_ptr rec, mktpool_ptr mkt)
            : n_instr(ins.num_instr()), n_record(), n_trade(),
              instrument(ins), position(pos), funds(funds),
              record(rec), mkt(mkt)
        {
            assert(ins.num_instr() == pos.num_instr());
            assert(pos.num_client() == funds.num_client());
        }

        auto num_instr() const { return n_instr; }
        auto num_record() const { return n_record; }
        auto num_trade() const { return n_trade; }

        auto &get_ins() { return instrument; }
        const auto &get_ins() const { return instrument; }
        auto &get_ins(id_type ins_id) { return instrument.get(ins_id); }
        const auto &get_ins(id_type ins_id) const { return instrument.get(ins_id); }

        auto &get_pos() { return position; }
        const auto &get_pos() const { return position; }
        auto &get_pos(id_type cli_id, id_type ins_id) { return position.get(cli_id, ins_id); }
        const auto &get_pos(id_type cli_id, id_type ins_id) const { return position.get(cli_id, ins_id); }

        auto &get_funds() { return funds; }
        const auto &get_funds() const { return funds; }
        auto &get_funds(id_type cli_id) { return funds.get(cli_id); }
        const auto &get_funds(id_type cli_id) const { return funds.get(cli_id); }

        void on_submit(const order_type &order) override
        {
            auto time = mkt->get(order.ins_id).time;
            if (order.is_buy())
            {
                on_submit_buy(time, order.cli_id, order.ins_id, order.seq_id,
                              order.price, order.quant, order.is_limit());
            }
            else
            {
                on_submit_sell(time, order.cli_id, order.ins_id, order.seq_id,
                               order.price, order.quant, order.is_limit());
            }
        }

        void on_match(const order_type &order, quant_type price, quant_type quant, quant_type tnvr, bool is_aggr) override
        {
            auto time = mkt->get(order.ins_id).time;
            if (order.is_buy())
            {
                on_match_buy(time, order.cli_id, order.ins_id, order.seq_id, price, quant, tnvr, is_aggr);
            }
            else
            {
                on_match_sell(time, order.cli_id, order.ins_id, order.seq_id, price, quant, tnvr, is_aggr);
            }
        }

        void on_queue(const order_type &order) override
        {
            // nothing to track when an order is queued
        }

        void on_cancel(const order_type &order, quant_type quant) override
        {
            id_type cli_id = order.cli_id;
            id_type ins_id = order.ins_id;
            auto &pool = mkt->get(ins_id);

            n_record += 1;
            auto rec = record_type::create(pool.time, cli_id, ins_id, order.seq_id,
                                           order.price, quant, order.price * quant,
                                           RecordEvent::cancel, false, false);
            record->on_record(cli_id, ins_id, rec);

            if (order.is_buy())
            {
                auto &cash = funds.get(cli_id);
                cash.avail += order.is_limit() ? order.price * quant : pool.price * quant;
                record->on_funds(cli_id, ins_id, cash);
            }
            else
            {
                auto &pos = position.get(cli_id, ins_id);
                pos.avail += quant;
                record->on_position(cli_id, ins_id, pos);
            }
        }

        // risk mgr

        quant_type equity0(id_type cli_id) const
        {
            quant_type v = funds.get(cli_id).net0;
            for (id_type ins = 0; ins < n_instr; ++ins)
            {
                v += position.get(cli_id, ins).net0 * mkt->get0(ins).price;
            }
            return v;
        }

        quant_type equity(id_type cli_id) const
        {
            quant_type v = funds.get(cli_id).net;
            for (id_type ins = 0; ins < n_instr; ++ins)
            {
                v += position.get(cli_id, ins).net * mkt->get(ins).price;
            }
            return v;
        }

        quant_type debt0(id_type cli_id) const
        {
            quant_type debt = funds.get(cli_id).borrow0;
            for (id_type ins = 0; ins < n_instr; ++ins)
            {
                debt += position.get(cli_id, ins).borrow0 * mkt->get0(ins).price;
            }
            return debt;
        }

        quant_type debt(id_type cli_id) const
        {
            quant_type debt = funds.get(cli_id).borrow;
            for (id_type ins = 0; ins < n_instr; ++ins)
            {
                debt += position.get(cli_id, ins).borrow * mkt->get(ins).price;
            }
            return debt;
        }
    };
}
